using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using igp.net.DataModel;

namespace igp.net.DataModel
{
    /// <summary>
    /// This is an abstract class that is derived by IgpFunction and igpPolynomial.
    /// </summary>
    public abstract class IgpFunctionBase : IgpItem
    {
        /// <summary>
        /// Default constructor.
        /// </summary>
        protected IgpFunctionBase()
        {
            EditMode = IgpFunctionInteractMode.Evaluate;
        }

        /// <summary>
        /// Function's domain lower x value.
        /// </summary>
        public double DomainLower { get; set; }

        /// <summary>
        /// Function's domain higher x value.
        /// </summary>
        public double DomainHigher { get; set; }

        /// <summary>
        /// Enables/Disables function domain limitaion.
        /// </summary>
        public bool Limited { get; set; }

        /// <summary>
        /// Determines the way this function will interact with the user.
        /// <seealso cref="IgpFunctionInteractMode"/>
        /// </summary>
        public IgpFunctionInteractMode EditMode { get; set; }

        /// <summary>
        /// This method draws a function on the screen.
        /// </summary>
        /// <param name="graph">Graphics object used for drawing.</param>
        public override void Draw(Graphics graph)
        {
            double tx;

            int ler = 0;

            var point = new Point();
            var pointOld = new Point();

            point.X = 0;
            point.Y = 0;

            double step = (1/igpSettings.Singleton.DrawingDetails);
            double starter, finish;

            IgpView View = IgpProject.Singleton.View;

            if (DomainLower == DomainHigher)
            {
                starter = 0;
                finish = View.Width;
            }
            else
            {
                starter = Math.Max(View.GetVScreenCoordinateX(DomainLower), 0.0);
                finish = Math.Min(View.GetVScreenCoordinateX(DomainHigher - 0.0000000001), View.Width);
            }

            for (tx = starter; tx <= finish; tx += step)
            {
                const int er = 0;

                //double txc = tx*View.GetRatioX();

                double ty = tx == starter
                                ? Value(View.GetRealCoordinateX((int) tx + 1))
                                : Value(View.GetRealCoordinateX((int) tx));

                pointOld.X = point.X;
                pointOld.Y = point.Y;

                point.X = View.GetScreenCoordinateX(View.GetRealCoordinateX((int)tx));
                point.Y = View.GetScreenCoordinateY(ty);

                if (tx == starter || (ler != 0))
                {
                    pointOld.X = point.X;
                    pointOld.Y = point.Y;
                }

                try
                {
                    if (
                        !((point.Y <= 0 && pointOld.Y >= IgpProject.Singleton.View.Height) ||
                          (pointOld.Y <= 0 && point.Y >= IgpProject.Singleton.View.Height)))
                    {
                        graph.DrawLine(new Pen(LineColor, LineWidth), pointOld, point);
                    }
                }
                catch (OverflowException)
                {
                }

                ler = er;
            }

            if (tx != finish)
            {
                pointOld.X = point.X;
                pointOld.Y = point.Y;

                point.X = View.GetScreenCoordinateX(View.GetRealCoordinateX((int)finish));
                point.Y = View.GetScreenCoordinateY(Value(View.GetRealCoordinateX((int) finish)));

                try
                {
                    if (
                        !((point.Y <= 0 && pointOld.Y >= IgpProject.Singleton.View.Height) ||
                          (pointOld.Y <= 0 && point.Y >= IgpProject.Singleton.View.Height)))
                    {
                        graph.DrawLine(new Pen(LineColor, LineWidth), pointOld, point);
                    }
                }
                catch (OverflowException)
                {
                }
            }
        }

        /// <summary>
        /// Draws a fragment of the function in the range of [ a ; b ].
        /// </summary>
        /// <param name="graph">Graphics used for painting</param>
        /// <param name="a">range lower x coordinate</param>
        /// <param name="b">range higher x coordinate</param>
        /// <param name="col">line color</param>
        /// <param name="width">line wodth</param>
        protected void Draw(Graphics graph, double a, double b, Color col, int width)
        {
            if (!IsVisible) return;

            double tx;
            int ler = 0;

            var punkt = new Point();
            var punktold = new Point();

            punkt.X = 0;
            punkt.Y = 0;

            double dokladnosc = 0.2;
            if (dokladnosc > 1) dokladnosc = 1;
            if (dokladnosc < 0.001) dokladnosc = 0.001;


            double krok = (1/dokladnosc);
            double starter, finish;
            IgpView View = IgpProject.Singleton.View;

            if (a == b)
            {
                starter = 0;
                finish = View.Width;
            }
            else
            {
                starter = Math.Max(View.GetScreenCoordinateX(a), 0.0);
                finish = Math.Min(View.GetScreenCoordinateX(b - 0.0000000001), View.Width);
            }

            for (tx = starter; tx <= finish; tx += krok)
            {
                const int er = 0;

                double ty = tx == starter
                                ? Value(View.GetRealCoordinateX((int) tx + 1))
                                : Value(View.GetRealCoordinateX((int) tx));

                punktold.X = punkt.X;
                punktold.Y = punkt.Y;

                punkt.X = (int) tx;
                punkt.Y = View.GetScreenCoordinateY(ty);

                if (tx == starter || (ler != 0))
                {
                    punktold.X = punkt.X;
                    punktold.Y = punkt.Y;
                }

                try
                {
                    if (
                        !((punkt.Y <= 0 && punktold.Y >= IgpProject.Singleton.View.Height) ||
                          (punktold.Y <= 0 && punkt.Y >= IgpProject.Singleton.View.Height)))
                    {
                        graph.DrawLine(new Pen(col, width), punktold, punkt);
                    }
                }
                catch (OverflowException)
                {
                }

                ler = er;
            }

            if (tx != finish)
            {
                punktold.X = punkt.X;
                punktold.Y = punkt.Y;

                punkt.X = (int) finish;
                punkt.Y = View.GetScreenCoordinateY(Value(View.GetRealCoordinateX((int) finish)));
                try
                {
                    if (
                        !((punkt.Y <= 0 && punktold.Y >= IgpProject.Singleton.View.Height) ||
                          (punktold.Y <= 0 && punkt.Y >= IgpProject.Singleton.View.Height)))
                    {
                        graph.DrawLine(new Pen(col, width), punktold, punkt);
                    }
                }
                catch (OverflowException)
                {
                }
            }
        }

        /// <summary>
        /// This method is required for item's reaction to mouse movement. 
        /// You must invoke this base method in your derived class in order 
        /// to update some useful properties.
        /// </summary>
        /// <param name="graph">Graphics object that can be used to paint something on the screen</param>
        /// <param name="e">Mouse Event Details</param>
        public override void InteractMove(Graphics graph, MouseEventArgs e)
        {
            base.InteractMove(graph, e);

            if (EditMode == IgpFunctionInteractMode.Evaluate)
            {
                IgpProject.Singleton.Cursor.InteractMove(IgpProject.Singleton.Graph, e);
                double x = IgpProject.Singleton.View.GetRealCoordinateX(e.X);

                if (e.Button == MouseButtons.Left)
                {
                    double x0 = Math.Max(Math.Min(DomainHigher, x), DomainLower);
                    int cx = IgpProject.Singleton.View.GetScreenCoordinateX(x0);

                    FormMain.Singleton.EditorWindow.ManualEval(x0);

                    double df = Derivative(x0);
                    double f = Value(x0);
                    double x1 = IgpProject.Singleton.View.GetRealCoordinateX(cx - 50);
                    double x2 = IgpProject.Singleton.View.GetRealCoordinateX(cx + 50);
                    double y1 = df*x1 + f - df*x0;
                    double y2 = df*x2 + f - df*x0;

                    var p1 = new Point(
                        IgpProject.Singleton.View.GetScreenCoordinateX(x1),
                        IgpProject.Singleton.View.GetScreenCoordinateY(y1));

                    var p2 = new Point(
                        IgpProject.Singleton.View.GetScreenCoordinateX(x2),
                        IgpProject.Singleton.View.GetScreenCoordinateY(y2));

                    graph.DrawLine(new Pen(Color.Red), p1, p2);
                }
            }
            else if (e.Button == MouseButtons.Left && VectorX != 0)
            {
                InteractionInUse = true;

                int from = MouseWhenPressed.X;
                int to = MouseWhenMoving.X;

                int temp = from;

                if (from > to)
                {
                    from = to;
                    to = temp;
                }

                double dfrom = IgpProject.Singleton.View.GetRealCoordinateX(from);
                double dto = IgpProject.Singleton.View.GetRealCoordinateX(to);

                if ((dfrom < DomainLower && dto < DomainLower) || (dfrom > DomainHigher && dto > DomainHigher)) return;

                if (DomainLower != DomainHigher)
                {
                    dfrom = Math.Max(dfrom, DomainLower);
                    dto = Math.Min(dto, DomainHigher);
                }

                string str = "[ " + dfrom.ToString("0.00001") + " ; " + dto.ToString("0.00001") + " ]";
                int wid = graph.MeasureString(str, igpSettings.Singleton.ChartFont).ToSize().Width + 20;

                var tp = new Point(
                    Math.Max(Math.Min(from, IgpProject.Singleton.View.Width - wid - 10), 25),
                    Math.Max(
                        Math.Min(
                            IgpProject.Singleton.View.GetScreenCoordinateY(
                                Value(IgpProject.Singleton.View.GetRealCoordinateX(MouseWhenPressed.X))) - 50,
                            IgpProject.Singleton.View.Height - 40), 25));

                var re = new Rectangle(tp, new Size(wid, 30));
                var re2 = new Rectangle(new Point(tp.X + 3, tp.Y + 3), new Size(wid, 30));

                graph.FillRectangle(new SolidBrush(Color.FromArgb(100, Color.Black)), re2);
                graph.FillRectangle(new LinearGradientBrush(re, Color.White, Color.FromArgb(230, 230, 230), 0, false),
                                    re);
                graph.DrawRectangle(new Pen(Color.DarkGray), re);

                Draw(graph, dfrom, dto, Color.Red, 4);


                graph.DrawString(str, new Font("Calibri", 8), new SolidBrush(Color.Black), tp.X + 5, tp.Y + 15);

                if (EditMode == IgpFunctionInteractMode.FindRoot)
                {
                    graph.DrawString("findRoot>", new Font("Calibri", 8), new SolidBrush(Color.Black), tp.X + 5,
                                     tp.Y + 2);
                }
                else
                {
                    graph.DrawString("finExtremum>", new Font("Calibri", 8), new SolidBrush(Color.Black), tp.X + 5,
                                     tp.Y + 2);
                }
            }
        }

        /// <summary>
        /// This method is required for item's reaction to mouse buttons being released. 
        /// </summary>
        /// <param name="graph">Graphics object that can be used to paint something on the screen</param>
        /// <param name="e">Mouse Event Details</param>
        public override void InteractUp(Graphics graph, MouseEventArgs e)
        {
            base.InteractUp(graph, e);

            if (EditMode == IgpFunctionInteractMode.Evaluate)
            {
                IgpProject.Singleton.Cursor.InteractMove(IgpProject.Singleton.Graph, e);
            }
            else if (e.Button == MouseButtons.Left && VectorX != 0)
            {
                int from = MouseWhenPressed.X;
                int to = MouseWhenMoving.X;

                int temp = from;
                
                if (from > to)
                {
                    from = to;
                    to = temp;
                }

                double dfrom = IgpProject.Singleton.View.GetRealCoordinateX(from);
                double dto = IgpProject.Singleton.View.GetRealCoordinateX(to);

                if ((dfrom < DomainLower && dto < DomainLower) || (dfrom > DomainHigher && dto > DomainHigher)) return;

                if (DomainLower != DomainHigher)
                {
                    dfrom = Math.Max(dfrom, DomainLower);
                    dto = Math.Min(dto, DomainHigher);
                }

                if (EditMode == IgpFunctionInteractMode.FindRoot)
                {
                    try
                    {
                        double acc = FormMain.Singleton.EditorWindow.GetAccuracy();
                        double root = FindRoot(dfrom, dto, acc);
                        root = Math.Round(root, -(int) Math.Log10(acc));
                        FormMain.Singleton.LogMessage("root: " + root);
                        IgpLabel lab = IgpProject.Singleton.AddLabel();
                        lab.ItemName = "root: " + root;
                        lab.X = root;
                        lab.Y = Value(root);
                        lab.FixedX = false;
                        lab.FixedY = false;
                        FormMain.Singleton.PickerWindow.RefreshList();
                    }
                    catch (Exception ex)
                    {
                        FormMain.Singleton.LogMessage("findRoot: " + ex.Message);
                    }
                }
                else
                {
                    try
                    {
                        double acc = FormMain.Singleton.EditorWindow.GetAccuracy();
                        double extr = FindExtremum(dfrom, dto, acc);
                        extr = Math.Round(extr, -(int) Math.Log10(acc));
                        double derfrom = Derivative(dfrom);
                        double derto = Derivative(dto);
                        IgpLabel lab = IgpProject.Singleton.AddLabel();

                        lab.X = extr;
                        lab.Y = Value(extr);
                        lab.FixedX = false;
                        lab.FixedY = false;

                        if (derfrom > 0 && derto < 0)
                        {
                            FormMain.Singleton.LogMessage("maximum: " + extr);
                            lab.ItemName = "maximum: " + extr;
                        }
                        else if (derfrom < 0 && derto > 0)
                        {
                            FormMain.Singleton.LogMessage("minimum: " + extr);
                            lab.ItemName = "minimum: " + extr;
                        }

                        FormMain.Singleton.PickerWindow.RefreshList();
                    }
                    catch (Exception ex)
                    {
                        FormMain.Singleton.LogMessage("findExtremum: " + ex.Message);
                    }
                }
                InteractionInUse = false;
            }
        }

        /// <summary>
        /// Finds a root of the function in range [ x1 ; x2 ].
        /// This calculation is based on the Darboux poperty of continous functions.
        /// If not found an exception is thrown.
        /// </summary>
        /// <param name="x1">range beginning coordinate</param>
        /// <param name="x2">range finish coordinate</param>
        /// <param name="acc">desired accuracy</param>
        /// <returns>x coordinate of found root</returns>
        public double FindRoot(double x1, double x2, double acc)
        {
            double x3, y1, y2, y3;
            x3 = (x2 + x1)/2;
            y1 = Value(x1);
            y2 = Value(x2);
            y3 = Value(x3);

            if (y1*y2 < 0)
            {
                if (x2 - x1 < acc) return (x2 + x1)/2;

                if (y3 == 0) return x3;
                if (y1*y3 < 0) return FindRoot(x1, x3, acc);
                if (y2*y3 < 0) return FindRoot(x3, x2, acc);
            }
            else
            {
                double xt = FindExtremum(x1, x2, acc);
                double val = Value(xt);
                if (val < 0) val *= -1;
                if (val <= acc/100)
                {
                    return xt;
                }
            }

            throw new Exception("not found");
        }

        /// <summary>
        /// Finds an exremum of the function in range [ x1 ; x2 ].
        /// This calculation is based on the Darboux poperty of continous functions.
        /// If not found an exception is thrown.
        /// </summary>
        /// <param name="x1">range beginning coordinate</param>
        /// <param name="x2">range finish coordinate</param>
        /// <param name="acc">desired accuracy</param>
        /// <returns>x coordinate of found root</returns>
        public double FindExtremum(double x1, double x2, double acc)
        {
            double x3 = (x2 + x1)/2;
            double y1 = Derivative(x1);
            double y2 = Derivative(x2);
            double y3 = Derivative(x3);

            if (y1*y2 < 0)
            {
                if (x2 - x1 < acc) return (x2 + x1)/2;

                if (y3 == 0) return x3;
                if (y1*y3 < 0) return FindExtremum(x1, x3, acc);
                if (y2*y3 < 0) return FindExtremum(x3, x2, acc);
            }

            throw new Exception("not found");
        }

        /// <summary>
        /// Calculates the value of the function for the desired argument.
        /// </summary>
        /// <param name="x">argument</param>
        /// <returns>f(x)</returns>
        public abstract double Value(double x);

        /// <summary>
        /// Calculates the value of the functions' indefinite integral for the desired argument.
        /// The remaining constant is assumed to be 0.
        /// </summary>
        /// <param name="x">argument</param>
        /// <returns>indefinite integral(x)</returns>
        public abstract double Integral(double x);

        /// <summary>
        /// Calculates the value of the functions' derivative for the desired argument.
        /// </summary>
        /// <param name="x">argument</param>
        /// <returns>f'(x)</returns>
        public abstract double Derivative(double x);
    }
}